import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.*;
import java.util.ArrayList;
import java.util.Stack;

import javax.swing.JPanel;

/**
 * @author Zach Yau
 */
public class Project3Panel extends JPanel implements KeyListener, MyBetterGL {

   // instance variables to support interactive model inspection 
   double rotX, rotY, rotZ;
   Graphics canvasGraphics;
   boolean perspective = false; // perspective or orthogonal projection
   boolean sphere = false;  // draw sphere or draw cube
   
   //My stuff
   ArrayList<Points> lineList = new ArrayList();
   Stack<double[]> matrixStack = new Stack();
   double[] currentMatrix;
   int width;
   int height;
   double[] scanLine;
   double[] light_dir;
   double[] normal;
   int[] color;
   int[] real_color;
   double[] zbuffer;

   private class Points{
	   double x;
	   double y;
	   double z;
	   Points(double x, double y, double z){
		   double[] newPoint = squish(x, y, z);
		   this.x = (width / 2) + ((width / 2) * newPoint[0]);
		   this.y = (height / 2) - ((height / 2) * newPoint[1]);
		   this.z = newPoint[2];
	   }
   }
   
   private double[] squish(double x, double y, double z){
	   double[] newPoint = new double[]{x, y, z, 1};
	   double[] result = new double[4];
	   for(int i = 0; i < 4; i++){
		   result[0] += currentMatrix[i] * newPoint[i]; 
		   result[1] += currentMatrix[i + 4] * newPoint[i]; 
		   result[2] += currentMatrix[i + 8] * newPoint[i];
		   result[3] += currentMatrix[i + 12] * newPoint[i];
	   }
	   if(perspective){
		   result[0] *= 1/result[3];
		   result[1] *= 1/result[3];
		   result[2] *= 1/result[3];
	   }
	   return result;
   }
   
   private double[] matrixMult4x4(double[] one, double[] two){
	   double[] result = new double[16];
	   for(int i = 0; i < 4; i++)
		   for(int j = 0; j < 4; j++)
			   for(int counter = 0; counter < 4; counter++)
				   result[i*4 + j] += one[i * 4 + counter] * two [j + counter * 4];
	   return result;
   }
   
   public void glViewport(int x, int y) {
	   width = x;
	   height = y;
	   fWidth = x;
	   fHeight = y;
   }

   public void glDirectionToLight(double x, double y, double z) {
	   light_dir = new double[3];
	   light_dir[0] = x;
	   light_dir[1] = y;
	   light_dir[2] = z;
   }

   public void glLoadIdentity() {
	   currentMatrix = new double[]{
		  1,0,0,0,
		  0,1,0,0,
		  0,0,1,0,
		  0,0,0,1};
   }

   public void glFrustum(double l, double r,
         double b, double t,
         double n, double f) {
	   double[] frustumMatrix = new double[]{
		   (n)/(r), 0, 0, 0,
		    0, (n)/(t), 0, 0,
		    0, 0, -(f+n)/(f-n), -(2*f*n)/(f-n),
		    0, 0, -1, 0};
	   currentMatrix = matrixMult4x4(currentMatrix, frustumMatrix);
   }

   public void glOrtho(double l, double r,
         double b, double t,
         double n, double f) {
	   double[] orthoMatrix = new double[]{
		   2/(r-l), 0, 0, -(r+l)/(r-l),
		   0, 2/(t-b), 0, -(t+b)/(t-b),
		   0, 0, 2/(n-f), -(f+n)/(f-n),
		   0, 0, 0, 1};
	   currentMatrix = matrixMult4x4(currentMatrix, orthoMatrix);
   }        

   public void glTranslate(double dx, double dy, double dz) {   
	   double[] transMatrix = new double[]{
		   1,0,0,dx,
		   0,1,0,dy,
		   0,0,1,dz,
		   0,0,0,1};
	   currentMatrix = matrixMult4x4(currentMatrix, transMatrix);        
   }

   public void glScale(double sx, double sy, double sz) { 
	   double[] scaleMatrix = new double[]{
		   sx,0,0,0,
		   0,sy,0,0,
		   0,0,sz,0,
		   0,0,0,1};
	   currentMatrix = matrixMult4x4(currentMatrix, scaleMatrix);         
   }  

   public void glRotate(double angle, double x, double y, double z){
	  double[] rotate = null;
	  double radAngle = Math.toRadians(angle);
      if(x == 1 && y ==0 && z == 0){
    	  rotate = new double[]{
    		  1,0,0,0,
	    	  0,Math.cos(radAngle),-Math.sin(radAngle),0,
	    	  0,Math.sin(radAngle),Math.cos(radAngle),0,
	    	  0,0,0,1};
      }
      else if(x == 0 && y ==1 && z == 0){
    	  rotate = new double[]{
    		  Math.cos(radAngle),0,Math.sin(radAngle),0,
        	  0,1,0,0,
        	  -Math.sin(radAngle),0,Math.cos(radAngle),0,
        	  0,0,0,1};
      }
      else if(x == 0 && y ==0 && z == 1){
    	  rotate = new double[]{
    		  Math.cos(radAngle),-Math.sin(radAngle),0,0,
    		  Math.sin(radAngle),Math.cos(radAngle),0,0,
        	  0,0,1,0,
        	  0,0,0,1};
      }
      else
    	  System.out.println("Durr...");
	  currentMatrix = matrixMult4x4(currentMatrix, rotate);
   }
   
   public void glNormal(double x, double y, double z){
	   double[] newRotate = new double[16];
	   normal = new double[3];
	   
	   double radAngle = Math.toRadians(rotX);
	   
	   double[] rotate1 = new double[]{
			   1,0,0,0,
			   0,Math.cos(radAngle),-Math.sin(radAngle),0,
			   0,Math.sin(radAngle),Math.cos(radAngle),0,
			   0,0,0,1};
	   
	   radAngle = Math.toRadians(rotY);
	   
	   double[] rotate2 = new double[]{
			   Math.cos(radAngle),0,Math.sin(radAngle),0,
			   0,1,0,0,
			   -Math.sin(radAngle),0,Math.cos(radAngle),0,
			   0,0,0,1};
	   
	   radAngle = Math.toRadians(rotZ);
	   
	   double[] rotate3 = new double[]{
			   Math.cos(radAngle),-Math.sin(radAngle),0,0,
			   Math.sin(radAngle),Math.cos(radAngle),0,0,
			   0,0,1,0,
			   0,0,0,1};
	   
	   newRotate = matrixMult4x4(rotate1, rotate2);
	   newRotate = matrixMult4x4(newRotate, rotate3);
	   
	   normal[0] = newRotate[0] * x + newRotate[1] * y + newRotate[2] * z;
	   normal[1] = newRotate[4] * x + newRotate[5] * y + newRotate[6] * z;
	   normal[2] = newRotate[8] * x + newRotate[9] * y + newRotate[10] * z; 
   }
   
   public double dotProd(double[] light, double[] normal){
	   double product = 0;
	   for(int i = 0; i < 3; i++){
		   product += light[i] * normal[i];
	   }
	   return product;
   }
   
   public void glColor(double r, double g, double b, double a) {
	   color = new int[4];
	   color[0] = (int) ((r) * 255);
	   color[1] = (int) ((g) * 255);
	   color[2] = (int) ((b) * 255);
	   color[3] = (int) ((a) * 255);
   }

   public void glBegin() {
	   scanLine = new double[fHeight*4];
	   lineList = new ArrayList();
   }

   public void glVertex(double x, double y, double z) {
	      lineList.add(new Points(x, y, z));
   }

   public void glEnd() {
	   int i = lineList.size()-1;
	   while(i > 0){
		   drawLine(lineList.get(i).x, lineList.get(i).y, 
				   lineList.get(i-1).x, lineList.get(i-1).y, 
				   lineList.get(i).z, lineList.get(i-1).z);
		   i--;
	   }
	   
	   drawLine(lineList.get(0).x, lineList.get(0).y,
			   lineList.get(lineList.size()-1).x, lineList.get(lineList.size()-1).y,
			   lineList.get(0).z, lineList.get(lineList.size()-1).z);
	   
	   double scaler = dotProd(light_dir, normal);
	   
	   real_color = new int[3];
	   real_color[0] = (int) (color[0] * Math.abs(scaler));
	   real_color[1] = (int) (color[1] * Math.abs(scaler));
	   real_color[2] = (int) (color[2] * Math.abs(scaler));
	   
	   scanLine();
   }    
   
   public void drawLine(double x1, double y1, double x2, double y2, double z1, double z2){
	   double m = (y1 - y2) / (x1 - x2);
	   double zxm = (z1 - z2) / (x1 - x2);
	   double zym = (z1 - z2) / (y1 - y2);
	   if(Math.abs(m) > 1){
		   if(y1 <= y2)
			   fillScanLine(y1, y2, x1, z1, m, zxm, zym, false);
		   else if(y1 > y2)
			   fillScanLine(y2, y1, x2, z2, m, zxm, zym, false);
	   }else{
		   if(x1 <= x2)
			   fillScanLine(x1, x2, y1, z1, m, zxm, zym, true);
		   else if(x1 > x2)
			   fillScanLine(x2, x1, y2, z2, m, zxm, zym, true);
	   }
   }
   
   public void fillScanLine(double fromLow, double toHigh,
		   double startLow, double z,
		   double m, double zxm, double zym, boolean xBased){
	   double y, x;
	   if(xBased){
		   x = (int)Math.round(fromLow);
		   y = startLow;
	   }
	   else{
		   y = (int)Math.round(fromLow);
		   x = startLow;
	   }
	   int low = (int)Math.round(fromLow);
	   
	   while(low <= (int)Math.round(toHigh)){
		   int tempY = (int)Math.round(y);
		   if(scanLine[tempY*4] == 0){
			   scanLine[tempY*4] = x;
			   scanLine[tempY*4 + 2] = z;
		   }
		   else if(x < scanLine[tempY*4]){
			   if(scanLine[tempY*4] > scanLine[tempY*4+1]){
				   scanLine[tempY*4+1] = scanLine[tempY*4];
				   scanLine[tempY*4 + 3] = scanLine[tempY*4 + 2];
			   }
			   scanLine[tempY*4] = x;
			   scanLine[tempY*4 + 2] = z;
		   }
		   else if(scanLine[tempY*4+1] == 0){
			   scanLine[tempY*4+1] = x;
			   scanLine[tempY*4 + 3] = z;
		   }
		   else if(x > scanLine[tempY*4+1]){
			   scanLine[tempY*4+1] = x;
			   scanLine[tempY*4 + 3] = z;
		   }
		   
		   if(xBased){
			   x++;
			   y += m;
			   z += zxm;
		   }else{
			   y++;
			   x += 1/m;
			   z += zym;
		   }
		   low++;
	   }
   }
   
   public void scanLine(){
	   for(int i = 0; i < fHeight * 4; i+=4){
		   double zm = (scanLine[i + 2] - scanLine[i + 3]) / (scanLine[i] - scanLine[i + 1]);
		   if(scanLine[i] != 0){
			   int x = (int)Math.round(scanLine[i]);
			   double z = scanLine[i+2];
			   while(x <= scanLine[i+1]){
				   if(z <= zbuffer[(i/4)*width + x]){
					   writePixel(x, i/4, 0, (byte)real_color[0], (byte)real_color[1], (byte)real_color[2], (byte)color[3]);
					   zbuffer[(i/4)*width + x] = z;
				   }
				   z += zm;
				   x++;
			   }
		   }
	   }
   }

public void glClear() { 
	   zbuffer = new double[height * width];
	   for(int i = 0; i < zbuffer.length; i++){
		   zbuffer[i] = Double.POSITIVE_INFINITY;
	   }
       fPixels = new byte[fWidth * fHeight * 4];   // not the best clear.  What color is this?
   }      
   
   public void glFlush() {
       BufferedImage image = getImage();
       if (image != null)
            canvasGraphics.drawImage(image, 0, 0, this); 
   }
   
   public void glPushMatrix() {
      currentMatrix = matrixStack.pop();
   }

   public void glPopMatrix() {
      matrixStack.push(currentMatrix);
   }
   
   // instance fields to support creation of a BufferedImage
   byte[] fPixels = null;
   int fHeight = 500;   
   int fWidth = 500;
   
   // writes a single pixel into the raster (rectangular array of pixels)
   void writePixel(int x, int y, double z, byte red, byte green, byte blue, byte alpha) {
       int loc = (fHeight-y)*fWidth*4+x*4;
       fPixels[loc] = red;
       fPixels[loc+1] = green;
       fPixels[loc+2] = blue;
       fPixels[loc+3] = alpha;         
   }
   
   public BufferedImage getImage() {   
       // Create a BufferedIamge of the gray values in bytes.
       BufferedImage fBufferedImage =
            new BufferedImage(fWidth, fHeight, BufferedImage.TYPE_4BYTE_ABGR); 

       // Get the writable raster so that data can be changed.
       WritableRaster wr = fBufferedImage.getRaster();

        // Now write the byte data to the raster
       wr.setDataElements(0, 0, fWidth, fHeight, fPixels);
       return fBufferedImage;  
   }  
   
   
///////////////////////////////////////////////////////////////////////
//////////  Beginning of CLIENT CODE //////////////////////////////////
//////////  THIS CODE MUST BE PART OF YOUR SUBMITTED WORK!!!! /////////
//////////  Continues until end mark below                     ////////
///////////////////////////////////////////////////////////////////////   

   void doCube( 
         double x1, double x2, 
         double y1, double y2, 
         double z1, double z2) {

      boolean multiColor = true;
      
      // FRONT face
      glNormal(0,0,1);
      if (multiColor) glColor(1,0,0,1);
      glBegin();   
      glVertex(x1, y1, z1);
      glVertex(x2, y1, z1); 
      glVertex(x2, y2, z1); 
      glVertex(x1, y2, z1);   
      glEnd();    
      
      // BACK face
      glNormal(0,0,-1);
      if (multiColor) glColor(0,1,0,1);            
      glBegin();   
      glVertex(x1, y1, z2);
      glVertex(x2, y1, z2); 
      glVertex(x2, y2, z2); 
      glVertex(x1, y2, z2);   
      glEnd();  


      // LEFT face      
      glNormal(-1,0,0);
      if (multiColor) glColor(0,0,1,1);      
      glBegin();   
      glVertex(x1, y1, z1);
      glVertex(x1, y2, z1); 
      glVertex(x1, y2, z2); 
      glVertex(x1, y1, z2);   
      glEnd();       

      // RIGHT face
      glNormal(1,0,0);
      if (multiColor) glColor(1,0,1,1);         
      glBegin();   
      glVertex(x2, y1, z1);
      glVertex(x2, y2, z1); 
      glVertex(x2, y2, z2); 
      glVertex(x2, y1, z2);   
      glEnd();      
       

      // BOTTOM
      glNormal(0,-1,0);
      if (multiColor) glColor(1,1,0,1);         
      glBegin();   
      glVertex(x1, y1, z1);
      glVertex(x2, y1, z1); 
      glVertex(x2, y1, z2); 
      glVertex(x1, y1, z2);   
      glEnd();        

      // TOP  
      glNormal(0,1,0); 
      if (multiColor) glColor(0,1,1,1);            
      glBegin();   
      glVertex(x1, y2, z1);
      glVertex(x2, y2, z1); 
      glVertex(x2, y2, z2); 
      glVertex(x1, y2, z2);   
      glEnd();                                   

   }                          

   void cube() {
      doCube(-0.5, 0.5, -0.5, 0.5, 0.5, -0.5);     
   }                            

   void normalize(double[] vector) {
       double mag = 0;
       for (int i = 0; i < 3; i++) {
           mag += vector[i]*vector[i];
       }
       mag = Math.sqrt(mag);
       for (int i = 0; i < 3; i++) {
           vector[i] /= mag;
       }     
   }
   
   public void doSphere() {
        double r = 1.0;
        int stacks = 20;
        int slices = 20;        

           
        double rhd = (2*Math.PI/slices);
        double thd = (Math.PI/stacks);
        
        double theta = thd;        
        double rho;
         
        for (int j = 0; j < stacks-2; j++) {
            rho = rhd;
            for (int i = 0; i < slices; i++) {
                   
                glBegin();  
                glNormal(r*Math.sin(theta+thd/2)*Math.cos(rho+rhd/2),
                         r*Math.sin(theta+thd/2)*Math.sin(rho+rhd/2),
                         r*Math.cos(theta+thd/2));              
                glVertex(r*Math.sin(theta)*Math.cos(rho),
                         r*Math.sin(theta)*Math.sin(rho),
                         r*Math.cos(theta));
                glVertex(r*Math.sin(theta)*Math.cos(rho+rhd),
                         r*Math.sin(theta)*Math.sin(rho+rhd),
                         r*Math.cos(theta));
                glVertex(r*Math.sin(theta+thd)*Math.cos(rho+rhd),
                         r*Math.sin(theta+thd)*Math.sin(rho+rhd),
                         r*Math.cos(theta+thd));
                glVertex(r*Math.sin(theta+thd)*Math.cos(rho),
                         r*Math.sin(theta+thd)*Math.sin(rho),
                         r*Math.cos(theta+thd));                
                glEnd();
                
                rho += rhd;                
            }               
            theta += thd;
        }
        
        
        theta = Math.PI - thd;
        rho = 0;
        for (int i = 0; i < slices; i++) {
            glBegin();  
            glNormal(r*Math.sin(theta+thd/2)*Math.cos(rho+rhd/2),
                     r*Math.sin(theta+thd/2)*Math.sin(rho+rhd/2),
                     r*Math.cos(theta+thd/2));
            glVertex(r*Math.sin(theta)*Math.cos(rho),
                     r*Math.sin(theta)*Math.sin(rho),
                     r*Math.cos(theta));
            glVertex(r*Math.sin(theta)*Math.cos(rho+rhd),
                     r*Math.sin(theta)*Math.sin(rho+rhd),
                     r*Math.cos(theta));
            glVertex(r*Math.sin(theta+thd)*Math.cos(rho+rhd),
                     r*Math.sin(theta+thd)*Math.sin(rho+rhd),
                     r*Math.cos(theta+thd));
            glEnd();    
            rho += rhd;      
        }
        
        theta = 0;
        rho = 0;
        for (int i = 0; i < slices; i++) {
            glBegin();  
            glNormal(r*Math.sin(theta+thd/2)*Math.cos(rho+rhd/2),
                     r*Math.sin(theta+thd/2)*Math.sin(rho+rhd/2),
                     r*Math.cos(theta+thd/2));
            glVertex(r*Math.sin(theta)*Math.cos(rho),
                     r*Math.sin(theta)*Math.sin(rho),
                     r*Math.cos(theta));   
            glVertex(r*Math.sin(theta+thd)*Math.cos(rho+rhd),
                     r*Math.sin(theta+thd)*Math.sin(rho+rhd),
                     r*Math.cos(theta+thd));  
            glVertex(r*Math.sin(theta+thd)*Math.cos(rho),
                     r*Math.sin(theta+thd)*Math.sin(rho),
                     r*Math.cos(theta+thd));                               
            glEnd();
            rho += rhd;      
        }
   }  

   protected void paintComponent(Graphics g) {
   
      super.paintComponent(g); 
      canvasGraphics = g;

      Dimension dim = getSize();       
      glViewport(dim.width,dim.height);
      glClear();
      
      double[] lightDir = {0, 0, 10};
      normalize(lightDir);
      glDirectionToLight(lightDir[0],lightDir[1],lightDir[2]);
         
      glLoadIdentity();    
      
      if (perspective)
         glFrustum(-0.65,0.65,-0.65,0.65,1,4.5);
      else
         glOrtho(-1.5, 1.5, -1.5, 1.5, -10.0, 10.0);
      glTranslate(0,0,-2.5);  
      
      glRotate(rotX,1,0,0);
      glRotate(rotY,0,1,0);
      glRotate(rotZ,0,0,1);   
      
      glColor(0.5f,1,0.5f,1);  // greenish   
      if (sphere) {
          doSphere();
      } else {
          cube();
      }
            
      glFlush();

   }
   
   //////////////////////////////////////////////////////////////////////////
   ////////////////////////////  END OF CLIENT CODE ////////////////////////
   /////////////////////////////////////////////////////////////////////////
   
   
   //// Interactive KeyListener - supports model choice and inspection

   @Override
   public void keyPressed(KeyEvent key) {
      switch (key.getKeyCode()) {
      case KeyEvent.VK_P:
         perspective = true;
         repaint();
         break;
      case KeyEvent.VK_N:
         perspective = false;
         repaint();
         break;    
      case KeyEvent.VK_S:
         sphere = true;
         repaint();
         break;
      case KeyEvent.VK_C:
         sphere = false;
         repaint();
         break;                    
      case KeyEvent.VK_ESCAPE:
         System.exit(0);
         break;
      case KeyEvent.VK_LEFT:
         rotY -= 3;
         repaint();
         break;  
      case KeyEvent.VK_RIGHT:
         rotY += 3;
         repaint();
         break;  
      case KeyEvent.VK_UP:
         rotX -= 3;
         repaint();            
         break;  
      case KeyEvent.VK_DOWN:
         rotX += 3;
         repaint();
         break;  
      case KeyEvent.VK_PAGE_UP:
         rotZ += 3;
         repaint();
         break;
      case KeyEvent.VK_PAGE_DOWN:
         rotZ -= 3;
         repaint();
         break;                                                                                             
      default:
         //System.out.println(key.getKeyCode());
         break;
      }
   }

   @Override
   public void keyReleased(KeyEvent e) {
      //Auto-generated method stub

   }

   @Override
   public void keyTyped(KeyEvent e) {
      //Auto-generated method stub

   }
   
}
